package BNS.plumbing;

import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.PrintStream;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.Calendar;
import java.util.Vector;

import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.TableColumn;


/** This class is involved with registering clients and players.
	It carries out the mechanics of starting and stopping experiments (aka games).
	It is not involved in the actual execution of graph play dynamics.
	*/
public class SessionHost extends UnicastRemoteObject 
							implements SessionHostInterface, Runnable {
	/**
   * 
   */
  private static final long serialVersionUID = 1L;
  protected Vector<ClientLiaison> clients;
	protected BNSGameController game;
	protected WaitingGame waitgame;
	protected Supervisor supervisor;
	enum Action {nada,quit,inst};
	private Action action;
	public final String SESSION_LOG_NAME;
	
	protected PrintStream sessionLog;
	private static int nextClientID= 1001;
	
	/** Sets up a Supervisor panel, initializes the Library, etc. */
	public SessionHost(String sessionName) throws RemoteException {
		super();
		clients= new Vector<ClientLiaison>(100);
		supervisor= new Supervisor(this);
		action= Action.nada;
		System.err.println("Hello, supervisor. Host ready for clients");
		waitgame= new WaitingGame();
		
    String DATE_FORMAT = "yyyy-MM-dd"; //Refer Java DOCS for formats
    java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat(DATE_FORMAT);
    Calendar c1 = Calendar.getInstance();
    SESSION_LOG_NAME=sessionName+"_"+sdf.format(c1.getTime());
//		Library.init();
//		sessionLog= Library.newSessionLog();
		}
	
	/** TODO: eliminate this?? Replace with Server.main?? */
	public static void main(String[] args) throws RemoteException {
		SessionHost host= new SessionHost(args[0]);
		host.run();
		}
	
	/** mandated by Runnable interface.
		Performs the install and quit actions requested by Supervisor, 
		and keeps the Supervisor display refreshed. 
		*/
	public synchronized void run() {
	  sessionLog= Library.newSessionLog(SESSION_LOG_NAME);
		while (action!=Action.quit) {
			if (action==Action.inst) {
				BNSGameController gc= Library.getGame(supervisor);
				if (gc!=null) {
					String man= (String)supervisor.typeList.getSelectedItem();
					System.err.println("action "+action+" manager type: "+man);
					try {
						LiveManager mgr= null;
//						if (man.equals("develop")) 
//							mgr= new DevelDriver(this,gc,Library.getLog(supervisor));
//						if (man.equals("simulation")) 
//							mgr= new SimuDriver(this,gc,Library.getLog(supervisor));
						if (man.equals("live game")) mgr= new LiveManager(this,gc);
						if (mgr!=null) gc.installGame(supervisor.getLocation(),mgr,sessionLog); }
					catch(java.rmi.RemoteException e) { System.err.println("failed manager"); }
					}
				System.err.println("action "+action+" done");
				}
			else if (action!=Action.nada) System.err.println("action="+action+" not grokked");
			action= Action.nada;
			refreshSupervisor();
			try { wait(500); }
			catch(InterruptedException ie) {
				System.err.println("something interrupted the host"); }
			}
		shutSession();
		}
		
	/** called to obtain a list of Clients that are available to play a game.
		@param required the number of players needed. If there aren't this many 
			available when the method is called, it returns an empty Vector
		@return a Vector of ClientLiaison objects (representing remote Client objects)
		*/
	public Vector<ClientLiaison> recruits(int required) {
		if (game!=null) {
			System.err.println("You have a game in progress!");
			return null; }	// TODO: accommodate multiple games at a time
		Vector<ClientLiaison> list= new Vector<ClientLiaison>(required); 
		for (int i=0; i<clients.size(); i++) {
			ClientLiaison client= clients.elementAt(i);
			if (client.game==waitgame) list.add(client);
			}
		int missing= required-list.size();
		if (missing>0) {
			// warning: BNSGameControllers call this while running on Event Dispatch Thread,
			// but this dialog blocks until acknowledged.
			// TODO: Release the thread and accomplish on a different thread.
			waitgame.add("\nwaiting for "+ missing +" more clients to join\n");
			JOptionPane.showMessageDialog(null, 
						  "waiting for "+ missing +" more clients to join",
						  "alert", JOptionPane.ERROR_MESSAGE);
			return null;
			}
		supervisor.repaint();
		return list;
		}
	
	void shutSession() {	// end of session
		//float totalPayoffs= (float)0.0;
		synchronized(clients) {		// tell all clients to resign
			for(int clientNum= 0; clientNum<clients.size(); clientNum++) {
				ClientLiaison client= clients.elementAt(clientNum);
				if (client==null) continue;	// but treat clients even when empty
				synchronized(client) {
					client.dismiss();	// tell Client to resign and stop
//					totalPayoffs+= client.payoff;
//					System.out.println("total payoff "+ client.clientID +" "+ 
//									   client.payoff +"\t"+ client.playerName);
					}
				}
			}
//		System.out.println("total of all payoffs this session: $"+ totalPayoffs);
		try { Thread.sleep(2000); }	// wait for everyone to resign
		catch(InterruptedException ex) { System.err.println(ex); }
		// TODO: verify everyone resigned
//		try {UnicastRemoteObject.unexportObject((WaitingGameInterface)waitgame, false);}
//		catch(NoSuchObjectException e) {}
		System.err.println("session terminated");
		sessionLog.println("session terminated");
		System.exit(1);
		}
	
	
	/** The BNS client must call this method to announce its existence and to
		gain entry to the upcoming BNS games. If the returned value is null, 
		the client has been rejected from participation for some reason. */
	public synchronized ClientLiaisonInterface register(String clientName) throws RemoteException {
		// TODO: synchronize prints so different threads don't clobber each other's messages.
		sessionLog.print(clientName+" registering; ");
		ClientLiaison client= null;
		synchronized(clients) {
			// TODO: check if this IP or name has already registered
			for (int cID=0; cID<clients.size(); cID++) {
				if ((clients.elementAt(cID).empty || clients.elementAt(cID).tardy)
				&& clientName.equals(clients.elementAt(cID).playerName)) {
					client= clients.elementAt(cID);
					client.reAdoptGame();
					client.empty= false;
					client.sendMessage("Client re-accepted into session; Notify supervisor.");
					System.err.println("re-accepting clientID= "+client.clientID
									   +" "+clients.elementAt(cID).playerName);
					sessionLog.println("re-accepted clientID= "+client.clientID
									   +" "+clients.elementAt(cID).playerName);
					}
				}
			if (client==null) {	// new player entering; sign him up with new ClientLiaison.
				int clientID= nextClientID++;
				//System.err.println("accepting as clientID= "+clientID);
				sessionLog.println("accepted as clientID= "+clientID);
				client= new ClientLiaison(clientName, clientID);
				client.adoptGame((BNSGameController)waitgame, null);
				clients.add(client);
				//System.err.println("clients size now "+clients.size());
//				activeClients++;
				}
			}
		supervisor.dataChange();
		return client;
		}
	
	/** Can be called by remote Clients to lodge a message directly with the SessionHost.
		Conceived as being useful for debugging.
		*/
	public void complain(int clientID, String message) throws RemoteException {}
	
	/** Called directly by remote Clients to announce their departure from the BNS session. */
	public void resign(ClientLiaisonInterface cl, String message) throws RemoteException {
		ClientLiaison client= (ClientLiaison)cl;
		// TODO: synchronize prints so different threads don't clobber each other's messages.
		if (false /*client.game==waitgame*/) {
			clients.remove(client);
			System.err.println("client "+client.playerName+" completely removed");
			}
		else {
			System.err.println("client "+client.playerName+" vanished; "+message);
			System.err.println(" game "+client.game.getTitle()+" left in limbo");
			sessionLog.println("client "+client.playerName+" vanished; "+message);
			sessionLog.println(" game "+client.game.getTitle()+" left in limbo");
			}
		supervisor.dataChange();
//		supervisor.repaint();	// Supervisor will see empty client
		}
		
	/** This routine checks each client to see if it has recently polled the
		SessionHost for an update. If it hasn't requested anything for too long
		then a message is printed, and a flag is posted in the ClientLiaison. 
		The Supervisor GUI can check the flag to alert the human 
		supervisor that something may be wrong with the machine. */
	private void refreshSupervisor() {
		long nowTime= System.currentTimeMillis();
		for (int i=0; i<clients.size(); i++) {
			ClientLiaison client= clients.elementAt(i);
			if (client==null) continue;
			synchronized(client) {
				long timeLag= nowTime-client.lastUpdate;
				boolean late= timeLag > 2000;
				if (late!=client.tardy) { 
					client.tardy= late;
					if (late) System.err.println("client "+ client.clientID +" is late "+ timeLag);
					else {
						System.err.println("client "+ client.clientID +" has resumed polling");
						client.empty= false;
						}
					//supervisor.repaint(); 
					}
				}
			}
		supervisor.repaint();
		}
		
	
	/** Implements a GUI that the human supervisor uses to control the BNS session. 
		It provides 3 input widgets (managementType, installButton, quitButton)
		and a list of clients with names and other status data.
		The managementType is a selector between these Manager classes: 
		DevelDriver (used for ease of game development),
		SimuDriver (used to re-execute a previously played game), and 
		LiveManager (used for normal live game play).
		*/
	public class Supervisor extends JFrame implements ActionListener {
		/**
     * 
     */
    private static final long serialVersionUID = 1L;
    protected JPanel cPanel;
		protected JButton inst, quit;
		protected JComboBox typeList;
		private String[] typeStrings= { "develop","simulation", "live game" };
			
		protected SessionHost host;
		private String[] colnames= new String[]
					{"clientID", "node", "T", "E","player name","game"};
		private AbstractTableModel dataModel;
		
//		public Supervisor() {}
		public Supervisor(SessionHost h) {
			host= h;
			setTitle("BNS Session Supervisor");
			cPanel= new JPanel();	// TODO: do I need this?
			typeList= new JComboBox(typeStrings);
			typeList.addActionListener(this);
			cPanel.add(typeList);
			
			inst= new JButton("install"); cPanel.add(inst); inst.addActionListener(this);
			quit= new JButton("quit");   cPanel.add(quit); quit.addActionListener(this);
			getContentPane().add(BorderLayout.NORTH,cPanel);
			dataModel= new AbstractTableModel() {
				/**
         * 
         */
        private static final long serialVersionUID = 1L;
        public int getColumnCount() { return 6; }
				public int getRowCount() { return clients.size();}
				public Object getValueAt(int row, int col) { 
					//System.err.println("value at "+row+", "+col);
					if (row>=clients.size()) return null;
					ClientLiaison client= clients.elementAt(row);
					if (client==null) return null;
					switch(col) {
					case 0: return new Integer(client.clientID);
					case 1: if (client.game==null || client.game==waitgame) return null;
							return client.eigenNode.lbl;
					case 2: return client.tardy? "!":" ";
					case 3: return client.empty? "*":" ";
					case 4: return client.playerName;
					case 5: return client.game.getTitle();
						}
					return new Integer(row*col); }
				public String getColumnName(int col) { return colnames[col]; }
				};
			JTable table = new JTable(dataModel);
			TableColumn tc;
			tc=table.getColumnModel().getColumn(0); tc.setMinWidth(40); tc.setMaxWidth(60);
			tc=table.getColumnModel().getColumn(1); tc.setMinWidth(30); tc.setMaxWidth(50);
			tc=table.getColumnModel().getColumn(2); tc.setMinWidth(10); tc.setMaxWidth(10);
			tc=table.getColumnModel().getColumn(3); tc.setMinWidth(10); tc.setMaxWidth(10);
			tc=table.getColumnModel().getColumn(4); tc.setPreferredWidth(100);
			JScrollPane scrollpane = new JScrollPane(table);
			getContentPane().add(BorderLayout.SOUTH,scrollpane);
			setSize(500,850);
			validate();
			pack();
			setLocation(820,20);
			//setLocationByPlatform(true);
			setVisible(true);
			}
		
		/** mandated by the ActionListener interface; handles various input widgets */
		public void actionPerformed(ActionEvent e) {
			Object src= e.getSource();
			//System.err.println("Supervisor action event "+src);
			if (src==inst) action= Action.inst;
			if (src==quit) action= Action.quit;
			if (src==typeList) {}
			synchronized(host) { host.notify(); }
			}
		
		/** called to prod the GUI to refresh its display. */
		public void dataChange() { dataModel.fireTableDataChanged(); }
		
		} // EOClass Supervisor
	
	} // EOC SessionHost
